Skip to content

fix(pbn): przepinaj osobę gdy PBN zmienił personId, zamiast ją pomijać#675

Open
mpasternak wants to merge 2 commits into
devfrom
fix/pbn-polonuuid-kolizja
Open

fix(pbn): przepinaj osobę gdy PBN zmienił personId, zamiast ją pomijać#675
mpasternak wants to merge 2 commits into
devfrom
fix/pbn-polonuuid-kolizja

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Problem

OsobaZInstytucji ma dwa klucze unikalne: personId (OneToOne na
Scientist) oraz polonUuid. To polonUuid — identyfikator z POL-onu — jest
stabilną tożsamością fizycznej osoby; personId PBN potrafi zmienić, np. po
scaleniu zdublowanych profili.

Import dopasowywał wiersz wyłącznie po personId, więc osoba wracająca pod
nowym personId leciała na INSERT i rozbijała się o unikalność polonUuid:

IntegrityError: duplicate key value violates unique constraint
  "pbn_api_osobazinstytucji_polonUuid_key"
DETAIL: Key ("polonUuid")=(0b0c6ad2-…) already exists.

Handler łapał to, raportował do Rollbara i pomijał osobę — czyli nowa
tożsamość PBN nigdy nie trafiała do bazy, a ten sam błąd wracał przy każdym
kolejnym imporcie. Rollbar
#1523, instalacja
bpp.ihit.waw.pl.

Rozwiązanie

Dopasowanie idzie najpierw po polonUuid, a personId jest zwykłym polem
do zaktualizowania — wiersz zostaje przepięty na nowy identyfikator PBN.

Raportowany zostaje tylko przypadek naprawdę niejednoznaczny: nowy personId
ma już swój wiersz z innym polonUuid. Scalenie dwóch tożsamości PBN to
decyzja o danych, nie poprawka techniczna — import to pomija i idzie dalej,
zamiast się przerywać.

Przy okazji znika kruche dopasowanie po treści komunikatu wyjątku
("polonUuid" in str(e)), które łapało również naruszenie NOT NULL dla osób
bez polonUuid i raportowało je jako „konflikt".

Testy

Nowy src/pbn_integrator/tests/test_osoba_z_instytucji.py — TDD: test
przepięcia najpierw padał (_zapisz_osobe_z_instytucji zwracało False).

  • nowy plik + test_integrator_import.py + test_importer_authors.py
    14 passed

🤖 Generated with Claude Code

https://claude.ai/code/session_01NcAqeqyqBzNEkkVnhpHDaH

mpasternak and others added 2 commits July 24, 2026 23:40
OsobaZInstytucji ma DWA klucze unikalne: personId (OneToOne na Scientist)
i polonUuid. To polonUuid — identyfikator z POL-onu — jest stabilną
tożsamością fizycznej osoby; personId PBN potrafi zmienić, np. po scaleniu
zdublowanych profili.

Import dopasowywał wiersz wyłącznie po personId, więc osoba wracająca pod
nowym personId leciała na INSERT i rozbijała się o unikalność polonUuid.
Handler łapał IntegrityError, raportował do Rollbara i POMIJAŁ osobę —
czyli nowa tożsamość PBN nigdy nie trafiała do bazy, a ten sam błąd wracał
przy każdym kolejnym imporcie (Rollbar #1523, bpp.ihit.waw.pl).

Teraz dopasowanie idzie najpierw po polonUuid, a personId jest zwykłym
polem do zaktualizowania. Zostaje raportowany tylko przypadek naprawdę
niejednoznaczny: nowy personId ma już swój wiersz z innym polonUuid —
scalenie dwóch tożsamości to decyzja o danych, nie poprawka techniczna.

Przy okazji znika kruche dopasowanie po treści komunikatu wyjątku
("polonUuid" in str(e)), które łapało też naruszenie NOT NULL dla osób
bez polonUuid i raportowało je jako "konflikt".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcAqeqyqBzNEkkVnhpHDaH
…rak polonUuid)

Uwagi z self-review poprzedniego commita:

- Rozszerzenie `except IntegrityError` na gołe łapanie było celowe (import
  całej kadry nie ma padać przez jedną osobę), ale komentarz twierdził, że
  trafia tam wyłącznie kolizja personId. Trafia tam też NOT NULL na polach
  przysłanych przez PBN jako null i każdy inny błąd integralności — a w
  Rollbarze wszystkie wyglądały identycznie. Dokładamy treść naruszonego
  ograniczenia do extra_data i prostujemy komentarz.

- Osoba bez polonUuid: zamiast dowiadywać się o tym okrężnie przez
  IntegrityError na NOT NULL i raportować jako "konflikt tożsamości",
  wychodzimy od razu z czytelnym logiem. Przy okazji znika ścieżka, w
  której pusty string leciał ValueError-em z UUIDField i wywracał cały
  import (call site nie jest osłonięty).

- Testy: dołożone asercje na przepięcie institutionId (na nim stoi scoping
  per-uczelnia), na stabilność pk (przepięcie, nie delete+create — FK
  deduplikator_autorow.main_osoba_z_instytucji ma SET_NULL) oraz round-trip
  wszystkich 9 pól. Obie asercje zweryfikowane mutacją kodu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcAqeqyqBzNEkkVnhpHDaH
@mpasternak

Copy link
Copy Markdown
Member Author

Poprawki po self-review

Review wychwycił trzy rzeczy w pierwszym commicie — wszystkie naniesione w 742dd318e:

1. Rozszerzone except IntegrityError bez uczciwej diagnostyki. Zamiana
if "polonUuid" in str(e) na gołe łapanie była celowa (import całej kadry nie
ma padać przez jedną osobę), ale komentarz twierdził, że trafia tam wyłącznie
kolizja personId. Trafia tam też NOT NULL na polach przysłanych przez PBN
jako null oraz każdy inny błąd integralności — a w Rollbarze wszystkie
wyglądały identycznie. Do extra_data idzie teraz treść naruszonego
ograniczenia, komentarz mówi prawdę.

2. Osoba bez polonUuid. Kolumna jest NOT NULL, więc guard if polon_uuid
niczego nie chronił — jedynym osiągalnym wynikiem był IntegrityError
raportowany pod mylącą etykietą „konflikt tożsamości". Teraz wychodzimy od razu,
z czytelnym logiem. Przy okazji znika ścieżka, w której pusty string leciał
ValueError-em z UUIDField i wywracał cały import (call site na linii 212
nie jest osłonięty).

3. Testy nie pilnowały tego, co refaktor faktycznie ruszył. Dołożone:

  • przepięcie institutionId (drugi Institution w teście) — na tym polu stoi
    scoping per-uczelnia w bpp/views/autocomplete/authors.py,
  • assert osoba.pk == pierwotny_pk — przepięcie, a nie delete+create; FK
    deduplikator_autorow.main_osoba_z_instytucji ma on_delete=SET_NULL, więc
    podmiana wiersza przeszłaby niezauważona,
  • round-trip wszystkich 9 pól (title, _from, _to nie były wcześniej
    w ogóle przekazywane przez helper).

Obie nowe asercje zweryfikowane mutacją kodu produkcyjnego — po usunięciu
osoba.institutionId = instytucja oraz po podmianie przepięcia na
delete()+create() testy padają. 4 passed po przywróceniu.

Świadomie NIE zrobione

  • Agregacja raportów Rollbara per przebieg importu (wzorem de82a0368
    dla DocxConversionError). Sensowne, ale to osobny temat — tu zostawiam
    raport per osoba, bo kolizja personId powinna być widoczna imiennie.
  • Sprzątanie „przegranego" wiersza przy kolizji personId — to scalanie
    tożsamości PBN, czyli decyzja o danych. Stan pozostaje trwały i taka osoba
    będzie raportowana przy każdym imporcie, dopóki ktoś nie rozstrzygnie
    ręcznie. Newsfragment został stonowany, żeby tego nie obiecywać.
  • select_for_update / advisory lock. _zapisz_osobe_z_instytucji biegnie
    w sekwencyjnej pętli po wyjściu z ThreadPoolExecutor (linia 212), a nie
    w puli — więc wyścig jest wąski (tylko równoległe management commands, celery
    chroni create_task_with_lock). W krytycznym przypadku wiersza jeszcze nie
    ma, więc select_for_update i tak nie miałby czego zablokować.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant